/*
 * Copyright (C) 2012 Damien Girard <dgirard@nativesoft.fr>
 * Copyright (C) 2012 Sebastien Baudrand <sebbaudrand@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package fr.umlv.qroxy.communication;

import fr.umlv.qroxy.cache.CacheInfo;
import fr.umlv.qroxy.cache.CacheRequest;
import fr.umlv.qroxy.cache.CacheUtilities;
import fr.umlv.qroxy.cache.CacheWriter;
import fr.umlv.qroxy.communication.transfer.ChunkedTransfer;
import fr.umlv.qroxy.communication.transfer.FixedTransfer;
import fr.umlv.qroxy.communication.transfer.SpeedCalculator;
import fr.umlv.qroxy.communication.transfer.Transfer;
import fr.umlv.qroxy.communication.transfer.TransferResult;
import fr.umlv.qroxy.http.*;
import fr.umlv.qroxy.rules.HeaderHttp;
import fr.umlv.qroxy.rules.Rule;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.*;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Connection between a client and a server, with a proxy between.
 *
 * This connection does not supports HTTP pipelining.
 *
 * @author Damien Girard <dgirard@nativesoft.fr>
 * @author Sebastien Baudrand <sebbaudrand@gmail.com>
 */
public class Connection implements Communicable {

    private static final Logger logger = Logger.getLogger(Connection.class.getName());
    /**
     * Size of the fragment to be read/sent on the network.
     */
    public static final int FRAGMENT_SIZE = 8092 * 8; // 8 Ko
    /*
     * Buffers.
     */
    private ByteBuffer clientEmissionBuffer = ByteBuffer.allocateDirect(FRAGMENT_SIZE);
    private ByteBuffer serverEmissionBuffer = ByteBuffer.allocateDirect(FRAGMENT_SIZE);
    /*
     * Communications channels.
     */
    private final SocketChannel clientChannel;
    private SocketChannel serverChannel;
    private String connectedServer = null;
    /*
     * Selection keys, in order to register interesting events.
     */
    private final SelectionKey clientSelectionKey;
    private SelectionKey serverSelectionKey;
    /*
     * State of the actual transfer.
     */
    private TransferState transferState = null;
    /*
     * Fields for cache operations.
     */
    private final String cacheDirectory;
    private CacheState cacheState = null;
    // DateFormat
    private DateFormat httpDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
    /*
     * Keepalive last reception date.
     */
    private Long timeoutTime = null;
    // Does the communication closed ?
    private boolean closed = false;

    /**
     * Class holdings the informations of a single HTTP entire process.
     */
    private class TransferState {

        private final HeaderHttp requestHeaderHttp;
        private HeaderHttp answerHeaderHttp = null;
        private Rule requestRule = null;
        private Rule answerRule = null;
        private Transfer clientToServerTransfer = null;
        private Transfer serverToClientTransfer = null;
        private boolean clientReadError = false;
        private boolean serverReadError = false;
        private SpeedCalculator clientSpeedCalculator = new SpeedCalculator();
        private SpeedCalculator serverSpeedCalculator = new SpeedCalculator();
        private long clientQoSamountOfDataToWrite = -1;
        private long serverQoSamountOfDataToWrite = -1;
        private long totalClientWrite = 0;
        private long totalServerWrite = 0;

        public TransferState(HeaderHttp requestHeaderHttp) {
            this.requestHeaderHttp = requestHeaderHttp;
        }
    }

    /**
     * Class holdings cache informations.
     */
    private class CacheState {

        boolean tryRead = false;
        String cacheHashedUrl = null;
        CacheRequest cacheRequest = null;
        CacheInfo cacheInfo = null;
        SocketChannel cacheSocketChannel = null;
        CacheWriter cacheWriter = null;

        /**
         * Closes everything inside this cache object. You must destroy it after
         * this operation, as its state will be invalid.
         */
        void close() {
            if (cacheSocketChannel != null) {
                try {
                    cacheSocketChannel.close();
                } catch (IOException ignore) {
                }
            }
            if (cacheWriter != null) {
                try {
                    cacheWriter.close(true);
                } catch (IOException ignore) {
                }
            }
        }
    }

    /**
     * Creates a new proxy connection.
     *
     * @param clientChannel SocketChannel of the client with the proxy server.
     * @param clientSelectionKey Selection key, used to register interesting
     * event. (READ/WRITE)
     */
    public Connection(SocketChannel clientChannel, SelectionKey clientSelectionKey, String cacheDirectory) {
        this.clientChannel = clientChannel;
        this.clientSelectionKey = clientSelectionKey;
        this.cacheDirectory = cacheDirectory;
    }

    /**
     * Closes this connection.
     */
    @Override
    public void close() {
        closed = true;

        clientSelectionKey.cancel();
        try {
            clientChannel.close();
        } catch (IOException ignore) {
            System.err.println("ClientChannel close error : " + ignore.getMessage());
        }
        if (serverSelectionKey != null) {
            serverSelectionKey.cancel();
        }
        if (serverChannel != null) {
            try {
                serverChannel.close();
            } catch (IOException ignore) {
                System.err.println("ServerChannel close error : " + ignore.getMessage());
            }
        }
        if (cacheState != null) {
            cacheState.close();
        }

        logger.log(Level.INFO, "Connection closed.");
    }

    /**
     * Closes this connection.
     *
     * @param message Message to send to the client.
     */
    public void close(ByteBuffer message) {
        // Trying to write the message.
        try {
            clientChannel.write(message);
        } catch (IOException e) {
        }

        // Closing the connection.
        close();
    }

    /**
     * Receive and parse an http header on a bytebuffer stream.
     *
     * This method allow to be called multiple time on the same buffer!
     *
     * @param channel Channel to read data from.
     * @param buffer Buffer to place data in.
     * @param useOnlyBuffer Do not read from the channel and use only data
     * contained in the buffer.
     *
     * @return An HTTP Header or null if there was not enough data to parse an
     * http header.
     */
    private HeaderHttp receiveHttpHeader(SocketChannel channel, ByteBuffer buffer, boolean useOnlyBuffer) throws IOException, HttpException {
        buffer.limit(buffer.capacity());

        // Reading availables data.
        if (!useOnlyBuffer) {
            if (channel.read(buffer) < 0) {
                close(); // General error, closing.
                return null;
            }
        }
        if (buffer.position() >= buffer.capacity()) {
            // Buffer full.
            throw new HttpException(HttpCodes.HTTP_ENTITY_TOO_LARGE, "HTTP header too long");
        }

        buffer.flip();
        // Does this buffer have an http header?
        int contentStartPosition = HeaderHttp.bufferHaveHttpHeader(buffer);
        if (contentStartPosition == -1) {
            return null;
        }

        buffer.rewind();
        HeaderHttp headerHttp = new HeaderHttp(buffer, contentStartPosition); // Parsing the HTTP header.

        // Reseting the buffer in an usable state for the next operations.
        buffer.rewind();
        buffer.position(buffer.limit());
        buffer.limit(buffer.capacity());

        return headerHttp;
    }

    /**
     * Reads the data from the client.
     *
     * Parses the HTTP request, connects to the distant server and do other
     * things.
     *
     * @param selector The selector in order to register the newly created
     * connection.
     * @param useOnlyBuffer Do not read from the channel and use only data
     * contained in the buffer.
     */
    public void performClientRead(Selector selector, boolean useOnlyBuffer) throws HttpException, IOException {
        logger.finest("client read - " + this.toString());
        if (closed) {
            return;
            //throw new RuntimeException("Invalid state. A read MUST not happen if the object is closed. Go back checks your code!");
        }

        // Reinit the emission buffer for reading.
        serverEmissionBuffer.limit(serverEmissionBuffer.capacity());

        // Do I have to read the http header in order to start a new transfer?
        if (transferState == null) {
            HeaderHttp headerHttp = receiveHttpHeader(clientChannel, serverEmissionBuffer, useOnlyBuffer);
            if (headerHttp == null) {
                return;
            }

            // Reseting the timeout timer.
            if (timeoutTime != null) {
                timeoutTime = null;
            }

            // Creation of the transfer state object and the cache state object.
            transferState = new TransferState(headerHttp);
            cacheState = new CacheState();

            // Getting the uri.
            String host = transferState.requestHeaderHttp.getProperties().get("Host");
            //if (host == null) {
            //    host = transferState.requestHeaderHttp.getUri().getHost();
            //}
            /*
             * if (host.endsWith("http")) // Removing the http ?? { host =
             * host.substring(0, host.length() - 4); }
             */
            if (host == null) {
                throw new HttpException(HttpCodes.HTTP_BAD_METHOD, "Undefined host.");
            }

            if ((serverChannel != null && serverChannel.isConnected() && connectedServer != null && !(connectedServer.equalsIgnoreCase(host)))
                    || serverChannel != null && serverChannel.isConnected() == false) {
                // Asking to connect to a server, but I am connected to a different one!
                // I must close the server connection and open a new one. (I cannot re-use the actual one)
                serverChannel.close();
                serverSelectionKey.cancel();
                serverChannel = null;
            }
            connectedServer = host;

            //int port = transferState.requestHeaderHttp.getUri().getPort();
            int port = 80;
            //if (port == -1) {
            //    port = 80;
            //}

            // Getting the rules associated with this HTTP Header
            transferState.requestRule = Qroxy.RULE_CHECKER.RulelookUp(transferState.requestHeaderHttp);
            if (transferState.requestRule != null) {
                logger.fine("Request rule "+ transferState.requestRule.getNameRule() +" associated with url: "+ transferState.requestHeaderHttp.getUrlToCall());
            }

            // Reducing or expanding the buffer.
            int newWeight = 3; // 3 is default weigth.
            if (transferState.requestRule != null && transferState.requestRule.getWeight() > 0) {
                newWeight = transferState.requestRule.getWeight();
                if (newWeight > 6) {
                    newWeight = 6;
                }
            }
            serverEmissionBuffer = shrinkOrExpandBuffer(newWeight, serverEmissionBuffer);

            // Handling the cache.
            cacheState.tryRead = CacheUtilities.isRequestedDataCanBeCached(transferState.requestHeaderHttp);
            if (cacheState.tryRead == true) {
                try {
                    // Getting the hashed url to get.
                    cacheState.cacheHashedUrl = CacheUtilities.hashUrl(transferState.requestHeaderHttp.getUrlToCall());

                    // Sending the cache request.
                    cacheState.cacheRequest = new CacheRequest(selector, this, cacheState.cacheHashedUrl);
                    cacheState.cacheRequest.sendRequest();
                } catch (NoSuchAlgorithmException ex) {
                    logger.warning("Error while hashing the URL. Disabling cache feature.");
                    cacheState.tryRead = false;
                }
            }

            // Establishing the connection with the distant server even if I have the data in cache.
            // In almost 80% of cases I need to revalidate against the server or the data is not in cache,
            // so I connect now to the server and I disconnect if it is not needed.
            if (serverChannel == null) { // if the server channel is already opened, I do not open a new one. (keep-alive)
                int interestOps = 0;

                serverChannel = SocketChannel.open();
                serverChannel.configureBlocking(false);
                if (serverChannel.connect(new InetSocketAddress(InetAddress.getByName(host), port)) == false) {
                    // Not connected. Having to connect later.
                    interestOps = SelectionKey.OP_CONNECT;
                }

                // I am interested in server writing.
                interestOps = interestOps | SelectionKey.OP_WRITE;

                // Registering the new connection.
                serverSelectionKey = serverChannel.register(selector, interestOps);
                serverSelectionKey.attach(this);
            } else {
                serverSelectionKey.interestOps(serverSelectionKey.interestOps() | SelectionKey.OP_WRITE);
            }

            // Setting the Transfer object.
            if (transferState.requestHeaderHttp.getTransferMethod() == HttpTransferMethods.CHUNKED) {
                transferState.clientToServerTransfer = new ChunkedTransfer(transferState.requestHeaderHttp.getEndOfHeaderInBuffer());
            } else {
                transferState.clientToServerTransfer = new FixedTransfer(transferState.requestHeaderHttp.getContentLength() + transferState.requestHeaderHttp.getEndOfHeaderInBuffer());
            }

            // If I have received a GET operation, then I don't have to read/write any data from the client.
            if (transferState.requestHeaderHttp.getHttpMethod() == HttpMethods.GET
                    || transferState.requestHeaderHttp.getHttpMethod() == HttpMethods.HEAD) {
                clientSelectionKey.interestOps(0);

                return;
            }
        }
        // Reading.
        TransferResult readResult = transferState.clientToServerTransfer.read(clientChannel, serverEmissionBuffer, useOnlyBuffer);

        if (readResult.getCount() < 0) {
            // Read problem ? Well it is not the read operation that handle close but the 
            // write, so letting it decide.
            serverSelectionKey.interestOps(serverSelectionKey.interestOps() | SelectionKey.OP_WRITE);
            transferState.clientReadError = true;
        }

        // Is there is data to be write to the server?
        if (readResult.isFinished()) {
            serverSelectionKey.interestOps(serverSelectionKey.interestOps() | SelectionKey.OP_WRITE);
        }
    }

    /**
     * Shrink or expand the buffer depending the
     */
    private ByteBuffer shrinkOrExpandBuffer(int newWeight, ByteBuffer currentBuffer) {
        // Reducing or expanding the cache.
        if (currentBuffer.capacity() != FRAGMENT_SIZE * newWeight) {
            System.out.println("Weigth: " + newWeight + " -> New buffer size: " + FRAGMENT_SIZE * newWeight);
            ByteBuffer newBuffer = ByteBuffer.allocate(FRAGMENT_SIZE * newWeight);
            currentBuffer.flip();
            newBuffer.put(currentBuffer);
            return newBuffer;
        }
        return currentBuffer;
    }

    /**
     * Reads the data from the server.
     *
     * And place them to the buffer to be sent to the client.
     */
    public void performServerRead(boolean useOnlyBuffer) throws IOException, HttpException {
        logger.finest("server read - " + this.toString());
        // Sanity check, a write must not happen if there is no http header.
        if (closed) {
            return;
            //throw new RuntimeException("Invalid state. A server read MUST not happen if the object is closed. Go back checks your code!");
        }
        if (transferState == null) {
            logger.warning("Read operation from server request without transfer state.");
            return;
            //throw new RuntimeException("Read operation from server request without transfer state.");
        }

        // Reinit the emission buffer for reading.
        clientEmissionBuffer.limit(clientEmissionBuffer.capacity());

        // Do I have to read the http header?
        if (transferState.answerHeaderHttp == null) {
            transferState.answerHeaderHttp = receiveHttpHeader(serverChannel, clientEmissionBuffer, useOnlyBuffer);
            if (transferState.answerHeaderHttp == null) {
                return;
            }

            // Getting the rules associated with this HTTP Header
            transferState.answerRule = Qroxy.RULE_CHECKER.RulelookUp(transferState.answerHeaderHttp);
            if (transferState.answerRule != null) {
                logger.fine("Answer rule "+ transferState.answerRule.getNameRule() +" associated with url: "+ transferState.answerHeaderHttp.getUrlToCall());
            }

            // Handling cache.
            if (serverChannel != cacheState.cacheSocketChannel) { // I don't do anything if the current channel is already the cache ones.
                // Checking if receiving HTTP 304 answer. (NOT MODIFIED)
                if (cacheState.tryRead == true && cacheState.cacheInfo != null
                        && transferState.answerHeaderHttp.getHttpCode() == 304) {
                    // Cache is valid! Reading data from the cache so.
                    switchToCacheChannel();

                    // Clearing the clientEmissionBuffer.
                    clientEmissionBuffer.clear();

                    // Return in order to force a new read of the http header etc...
                    return;
                } else if (cacheState.tryRead == true && CacheWriter.isDataCachable(transferState.answerHeaderHttp)) {
                    if (transferState.answerRule == null || transferState.answerRule.getPrivateData() == false) {
                        // Data can be cached.
                        try {
                            cacheState.cacheWriter = CacheWriter.createCacheWrite(cacheState.cacheHashedUrl, cacheDirectory, transferState.answerHeaderHttp);
                        } catch (Exception e) {
                            logger.info("Cannot open the cache file for write: " + cacheState.cacheHashedUrl + " - " + e);
                            cacheState.cacheWriter = null; // Cache is locked.
                        }
                    }
                }
            }
            // Todo: Reading interesting info from this header.

            // Reducing or expanding the buffer.
            int newWeight = 3; // 3 is default weigth.
            if (transferState.answerRule != null && transferState.answerRule.getWeight() > 0) {
                newWeight = transferState.answerRule.getWeight();
                if (newWeight > 6) {
                    newWeight = 6;
                }
            } else if (transferState.requestRule != null && transferState.requestRule.getWeight() > 0) {
                newWeight = transferState.requestRule.getWeight();
                if (newWeight > 6) {
                    newWeight = 6;
                }
            }
            clientEmissionBuffer = shrinkOrExpandBuffer(newWeight, clientEmissionBuffer);

            // Creation of the transfer object.
            if (transferState.answerHeaderHttp.getTransferMethod() == HttpTransferMethods.CHUNKED) {
                transferState.serverToClientTransfer = new ChunkedTransfer(transferState.answerHeaderHttp.getEndOfHeaderInBuffer(), cacheState.cacheWriter);
            } else {
                transferState.serverToClientTransfer = new FixedTransfer(transferState.answerHeaderHttp.getContentLength() + transferState.answerHeaderHttp.getEndOfHeaderInBuffer(), transferState.answerHeaderHttp.getEndOfHeaderInBuffer(), cacheState.cacheWriter);
            }
        }
        // Reading.
        TransferResult readResult = transferState.serverToClientTransfer.read(serverChannel, clientEmissionBuffer, useOnlyBuffer);


        if (readResult.getCount()
                < 0) {
            // Read problem ? Well it is not the read operation that handle close but the 
            // write, so letting it decide.
            clientSelectionKey.interestOps(clientSelectionKey.interestOps() | SelectionKey.OP_WRITE);
            transferState.serverReadError = true;
        }
        // Is there is data to be write to the client?


        if (readResult.isFinished()) {
            clientSelectionKey.interestOps(clientSelectionKey.interestOps() | SelectionKey.OP_WRITE);
        }
    }

    /**
     * Closes the serverSocketChannel and set the serverSocketChannel to the
     * cacheSocketChannel.
     */
    private void switchToCacheChannel() throws IOException {
        if (cacheState.cacheSocketChannel == null) {
            throw new RuntimeException("Cache socket channel is not set, this case must NEVER happen!");
        }
        if (cacheState.cacheSocketChannel.isConnected() == false) {
            // CacheSocket problem. Ignoring cache so.
            return;
        }
        if (serverSelectionKey != null) {
            serverSelectionKey.cancel();
        }
        if (serverChannel != null) {
            serverChannel.close();
        }

        serverChannel = cacheState.cacheSocketChannel;
        serverSelectionKey = cacheState.cacheSocketChannel.register(clientSelectionKey.selector(), SelectionKey.OP_READ);
        serverSelectionKey.attach(this);

        // Requesting the cache server to send the data.
        cacheState.cacheRequest.requestData();
    }

    /**
     * Writes the available datas to the server.
     *
     * @return True if the buffer is not empty and data must be parsed in order
     * to be sent.
     */
    public void performServerWrite() throws IOException, HttpException {
        logger.finest("server write - " + this.toString());
        // Sanity check, a write must not happen if there is no http header.
        if (closed) {
            return;
            //throw new RuntimeException("Invalid state. A server write MUST not happen if the object is closed. Go back checks your code!");
        }
        if (transferState == null) {
            //throw new RuntimeException("Write operation to server request without http transfer.");
            return;
        }

        // Handling the cache.
        // If cacheState.tryRead is true then I do not write to the server until the timeout 
        // is over or the cache connection has been set.
        if (cacheState.tryRead == true) {
            if (cacheState.cacheInfo == null) {
                if (cacheState.cacheRequest.isTimeout() == false) {
                    return;
                }
            } else {
                // I have got a cache answer!
                // Do I have to validate it?
                if (cacheState.cacheInfo.isMustRevalidate() == false) {
                    // I have to directly read cache data.
                    switchToCacheChannel();
                    return;
                }
            }
        }

        // Checking for inconsistent state. (serverChannel empty or serverChannel is connected to the cache)
        if (serverChannel == cacheState.cacheSocketChannel || serverChannel == null) {
            throw new RuntimeException("A server write operation must NEVER happen in cache transfer mode.");
        }

        // Handling the QoS for maximum emission.
        int writeLimit = -1;
        if (transferState.requestRule != null) {
            if (transferState.requestRule.getMaximalSpeed() > 0.0) {
                double speed = transferState.serverSpeedCalculator.updateSpeed(transferState.totalServerWrite, System.currentTimeMillis());
                if (speed >= 0) // Maximal amount of data allowed to be write in one seconds.
                {
                    transferState.serverQoSamountOfDataToWrite = (long) (transferState.requestRule.getMaximalSpeed() * 1000);
                }

                // Limiting the speed.
                writeLimit = (int) transferState.serverQoSamountOfDataToWrite;

                if (writeLimit <= 0) {
                    serverQOSwriteLocked = true;
                    serverSelectionKey.interestOps(serverSelectionKey.interestOps() & (~SelectionKey.OP_WRITE));
                    return;
                }
            }
        }

        // Sending data, only in the case that there is no cache.
        TransferResult writeResult = transferState.clientToServerTransfer.write(serverChannel, serverEmissionBuffer, writeLimit);
        if (writeResult.getCount() < 0) {
            logger.fine("Server write returned -1. Sending bad gateway http error and closing.");
            throw new HttpException(HttpCodes.HTTP_BAD_GATEWAY, "Server write returned -1.");
        }
        transferState.totalServerWrite += writeResult.getCount();
        transferState.serverQoSamountOfDataToWrite -= writeResult.getCount();

        // Handling end of communication.
        if (writeResult.isFinished() || transferState.clientReadError == true) {
            if (transferState.requestHeaderHttp.getCloseRule() == HttpCloseRules.CONNECTION_CLOSE_AFTER
                    || transferState.clientReadError == true) {
                clientChannel.socket().shutdownInput();
            } else {
                // Keep alive.
                // Nothing to do now. Maybe in future.
            }
            // I have to read data from the server.
            serverSelectionKey.interestOps(serverSelectionKey.interestOps() | SelectionKey.OP_READ);
        }

        if (writeResult.isFinished() == false && writeResult.hasMoreDataToWrite()) { // 
            serverSelectionKey.interestOps(serverSelectionKey.interestOps() | SelectionKey.OP_WRITE);
        } else {
            serverSelectionKey.interestOps(serverSelectionKey.interestOps() & (~SelectionKey.OP_WRITE));
        }

        // There is still data in the buffer, handling them.
        if (serverEmissionBuffer.position() != 0) {
            performClientRead(clientSelectionKey.selector(), true);
        }
    }
    private boolean clientQOSwriteLocked = false;
    private boolean serverQOSwriteLocked = false;

    /**
     * Unlock the QOS operation and relaunch the write.
     */
    public void unlockQOS() {
        if (clientQOSwriteLocked == true && clientSelectionKey != null && clientSelectionKey.isValid()) {
            clientSelectionKey.interestOps(clientSelectionKey.interestOps() | SelectionKey.OP_WRITE);
        }
        if (serverQOSwriteLocked == true && serverSelectionKey != null && serverSelectionKey.isValid()) {
            serverSelectionKey.interestOps(serverSelectionKey.interestOps() | SelectionKey.OP_WRITE);
        }
    }

    /**
     * Writes the available datas to the client.
     *
     * @return True if the buffer is not empty and data must be parsed in order
     * to be sent.
     */
    public void performClientWrite() throws IOException, HttpException {
        logger.finest("client write - " + this.toString());

        // Sanity check, a write must not happen if there is no http header.
        if (closed) {
            return;
            //throw new RuntimeException("Invalid state. A client write MUST not happen if the object is closed. Go back checks your code!");
        }
        if (transferState == null) {
            //throw new RuntimeException("Write operation to client request without http transfer.");
            return;
        }

        // Handling the QoS for maximum emission.
        int writeLimit = -1;
        if (transferState.answerRule != null) {
            if (transferState.answerRule.getMaximalSpeed() > 0.0) {
                double speed = transferState.clientSpeedCalculator.updateSpeed(transferState.totalClientWrite, System.currentTimeMillis());
                if (speed >= 0) // Maximal amount of data allowed to be write in one seconds.
                {
                    transferState.clientQoSamountOfDataToWrite = (long) (transferState.answerRule.getMaximalSpeed() * 1000);
                }

                // Limiting the speed.
                writeLimit = (int) transferState.clientQoSamountOfDataToWrite;

                if (writeLimit <= 0) {
                    clientQOSwriteLocked = true;
                    clientSelectionKey.interestOps(clientSelectionKey.interestOps() & (~SelectionKey.OP_WRITE));
                    return;
                }
            }
        }

        // Sending data.
        TransferResult writeResult = transferState.serverToClientTransfer.write(clientChannel, clientEmissionBuffer, writeLimit);
        if (writeResult.getCount() < 0) {
            logger.fine("Client write returned -1. Closing connection.");
            close();
            return;
        }
        // Storing the result for QoS.
        transferState.totalClientWrite += writeResult.getCount();
        transferState.clientQoSamountOfDataToWrite -= writeResult.getCount();

        // Handling end of communication.
        if (writeResult.isFinished()
                || transferState.serverReadError == true) {
            boolean wasReadFromCache = cacheState.cacheSocketChannel == serverChannel;

            // Closing the cache.
            if (writeResult.isFinished() == true && transferState.serverReadError == false && cacheState.cacheWriter != null) {
                // No read error? Then I can close properly the cache and write datas.
                cacheState.cacheWriter.close(false);
            }
            if (cacheState.cacheSocketChannel == serverChannel) {
                serverChannel = null;
            }
            cacheState.close();
            cacheState = null;

            // So, do I close or do I keep-alive?
            if (transferState.answerHeaderHttp.getCloseRule() == HttpCloseRules.CONNECTION_CLOSE_AFTER
                    || wasReadFromCache
                    || transferState.serverReadError == true
                    || clientChannel.socket().isInputShutdown()) {
                close();
                return;
            }

            // Keep alive.
            transferState = null; // New http transfer required.
            if (serverChannel != null) {
                serverSelectionKey.interestOps(0);
            }

            // Checking if I can read a client request, if no then it closed the
            // connection. (so no keep-alive)
            if (clientChannel.read(serverEmissionBuffer) == -1) {
                // No more data available.
                close();
                return;
            }

            // Marking the time, for the KeepAlive timeout.
            timeoutTime = System.currentTimeMillis() + HttpConstants.HTTP_KEEPALIVE_TIMEOUT;

            // I have to read data from the client.
            performClientRead(clientSelectionKey.selector(), true); // Reading data stored inside the buffer.
            clientSelectionKey.interestOps(clientSelectionKey.interestOps() | SelectionKey.OP_READ);
        }

        if (writeResult.isFinished() == false && writeResult.hasMoreDataToWrite()) { // 
            clientSelectionKey.interestOps(clientSelectionKey.interestOps() | SelectionKey.OP_WRITE);
        } else {
            clientSelectionKey.interestOps(clientSelectionKey.interestOps() & (~SelectionKey.OP_WRITE));
        }

        // There is still data in the buffer, handling it.
        if (clientEmissionBuffer.position() != 0) {
            performServerRead(true);
        }
    }

    /**
     * Gets the client channel.
     *
     * @return The client channel.
     */
    public SocketChannel getClientChannel() {
        return clientChannel;
    }

    /**
     * Gets the server channel.
     *
     * @return The server channel.
     */
    public SocketChannel getServerChannel() {
        return serverChannel;
    }

    /**
     * Gets the communication timeout time.
     *
     * @return The timeout time.s
     */
    public Long getTimeoutTime() {
        return timeoutTime;
    }

    public static void debugPrintBuffer(ByteBuffer buffer) {
        CharsetDecoder decoder = Charset.forName("ISO-8859-1").newDecoder();
        decoder.onUnmappableCharacter(CodingErrorAction.IGNORE);

        int position = buffer.position();
        int limit = buffer.limit();

        buffer.flip();

        CharBuffer outBuffer;
        try {
            outBuffer = decoder.decode(buffer);
        } catch (CharacterCodingException ex) {
            System.err.println("Blaaaaaaa !");
            return;
        }
        while (outBuffer.hasRemaining()) {
            char c = outBuffer.get();
            System.out.print(c);
        }
        System.out.print("EEEEEEEEEEEEEEEEEEEEEEEEEENNNNNNNNNNNNNNNNNNNNNNNDDDDDDDDDDDDDDDDDDDD\n"); // Flush.

        buffer.limit(limit);
        buffer.position(position);
    }

    @Override
    public void handleRead(SelectableChannel channel, SelectionKey selectionKey) {
        try {
            if (channel == clientChannel) {
                performClientRead(selectionKey.selector(), false);
            } else if (channel == serverChannel) {
                performServerRead(false);
            } else {
                throw new RuntimeException("UNKNOWN READ HANDLE. MUST NEVER HAPPEN!!!");
            }
        } catch (HttpException e) {
            logger.fine("Reading Http exception " + e);
            close(e.getPage());
        } catch (IOException e) {
            logger.log(Level.WARNING, "Reading exception. " + e);
            close();
        }
    }

    @Override
    public void handleWrite(SelectableChannel channel, SelectionKey selectionKey) {
        try {
            if (channel == clientChannel) {
                performClientWrite();
            } else if (channel == serverChannel) {
                performServerWrite();
            } else {
                throw new RuntimeException("UNKNOWN WRITE HANDLE. MUST NEVER HAPPEN!!!");
            }
        } catch (HttpException e) {
            close(e.getPage());
        } catch (IOException e) {
            // Exception.
            logger.log(Level.WARNING, "Writing exception. " + e);
            close();
        }
    }

    @Override
    public void handleAccept(SelectableChannel channel, Selector selector) {
        // Nothing to do.
        throw new RuntimeException("You must not use this call!");
    }

    @Override
    public void handleConnect(SelectableChannel channel, SelectionKey selectionKey) {
        if (selectionKey != serverSelectionKey) {
            throw new RuntimeException("SelectionKey is different than serverSelectionKey. This case muse NEVER happen.");
        }

        try {
            if (serverChannel.finishConnect() == false) {
                serverSelectionKey.interestOps(serverSelectionKey.interestOps() | SelectionKey.OP_CONNECT);
            } else {
                logger.log(Level.FINE, "Connected with distant server: " + ((SocketChannel) channel).socket().getRemoteSocketAddress());
                serverSelectionKey.interestOps(serverSelectionKey.interestOps() & (~SelectionKey.OP_CONNECT));
            }
        } catch (IOException e) {
            close(HttpCodes.HTTP_CLIENT_TIMEOUT.getPage());
        }
    }

    /**
     * Specify a cache connection.
     *
     * @param cacheInfo The informations about the received cache.
     * @param clientSocketChannel The socket channel used to receive data from
     * the cache.
     *
     * @return True if the cache has been correctly set, false otherwise if the
     * connection refuse this cache.
     */
    public boolean setCache(CacheInfo cacheInfo, SocketChannel cacheSocketChannel) {
        if (closed || cacheState == null || transferState == null) {
            logger.finest("Perform cache set closed object or null cacheState or null transferState.");
            return false;
        }

        cacheState.cacheInfo = cacheInfo;
        cacheState.cacheSocketChannel = cacheSocketChannel;

        // Adding cache fields to the header http.
        if (cacheInfo.isMustRevalidate()) {
            if (cacheInfo.geteTag().isEmpty() == false) {
                transferState.requestHeaderHttp.getProperties().put("If-None-Match", '"' + cacheInfo.geteTag() + '"');
            }
            if (cacheInfo.getExpirationDate() > 0) {
                transferState.requestHeaderHttp.getProperties().put("If-Modified-Since", httpDateFormat.format(new Date(cacheInfo.getExpirationDate())));
            }

            serverSelectionKey.interestOps(serverSelectionKey.interestOps() | SelectionKey.OP_WRITE);
        } else {
            try {
                // I have to directly read cache data.
                performServerWrite();
            } catch (IOException ex) {
                logger.warning("Set cache: " + ex);
            } catch (HttpException ex) {
                logger.warning("Set cache: " + ex); // Should never happen.
            }
        }

        return true;
    }
}
